﻿using System;
using System.Data;
using System.IO;
using System.Text.RegularExpressions;
using log4net;
using log4net.Core;
using MySql.Data.MySqlClient;

namespace Bridge.Base.Utilities
{
    /// <summary>
    /// Class for reporting an event to the database.
    /// </summary>
    public class BridgeReporter
    {
        /// <summary>
        /// Reports an event to the database.
        /// </summary>
        /// <param name="loggerName">Logger name that logged the event.</param>
        /// <param name="message">Message for the event.</param>
        /// <param name="level">Level severity of the event.</param>
        /// <exception cref="ArgumentNullException">Thrown if loggerName, message or level is null.</exception>
        /// <exception cref="ArgumentException">Thrown if loggerName or message is empty.</exception>
        public static void Report(string loggerName, string message, Level level)
        {
            if (loggerName == null) throw new ArgumentNullException("loggerName");
            if (message == null) throw new ArgumentNullException("message");
            if (level == null) throw new ArgumentNullException("level");
            if (loggerName.Length == 0) throw new ArgumentException("loggerName cannot be empty.", "loggerName");
            if (message.Length == 0) throw new ArgumentException("message cannot be empty.", "message");

            MySqlConnection connection = GetConnection();
            if (connection != null)
            {
                try
                {
                    MySqlCommand command = connection.CreateCommand();
                    command.CommandType = CommandType.Text;
                    command.CommandText = string.Format(
                        InsertEvent,
                        loggerName,
                        message,
                        "null",
                        level.Name);
                    command.ExecuteNonQuery();
                }
                catch (MySqlException ex)
                {
                    sLog.Fatal(string.Format("Could not report event on {0}, Database {1}.", ReportServer, ReportDatabase), ex);
                }
                finally
                {
                    try
                    {
                        connection.Close();
                        connection.Dispose();
                    }
                    catch (Exception) { }
                }
            }
        }

        /// <summary>
        /// Reports an event to the database.
        /// </summary>
        /// <param name="loggerName">Logger name that logged the event.</param>
        /// <param name="message">Message for the event.</param>
        /// <param name="level">Level severity of the event.</param>
        /// <param name="ex">Exception for the event.</param>
        /// <exception cref="ArgumentNullException">Thrown if loggerName, message or level is null.</exception>
        /// <exception cref="ArgumentException">Thrown if loggerName or message is empty.</exception>
        public static void Report(string loggerName, string message, Level level, Exception ex)
        {
            if (loggerName == null) throw new ArgumentNullException("loggerName");
            if (message == null) throw new ArgumentNullException("message");
            if (ex == null) throw new ArgumentNullException("ex");
            if (loggerName.Length == 0) throw new ArgumentException("loggerName cannot be empty.", "loggerName");
            if (message.Length == 0) throw new ArgumentException("message cannot be empty.", "message");

            MySqlConnection connection = GetConnection();
            if (connection != null)
            {
                try
                {
                    MySqlCommand command = connection.CreateCommand();
                    command.CommandType = CommandType.Text;
                    command.CommandText = string.Format(
                        InsertEvent,
                        loggerName,
                        message,
                        ReportException(connection, ex),
                        level.Name);
                    command.ExecuteNonQuery();
                }
                catch (MySqlException exception)
                {
                    sLog.Fatal(string.Format("Could not report event on {0}, Database {1}.", ReportServer, ReportDatabase), exception);
                }
                finally
                {
                    try
                    {
                        connection.Close();
                        connection.Dispose();
                    }
                    catch (Exception) { }
                }
            }
        }

        private static int? ReportException(MySqlConnection connection, Exception ex)
        {
            int? innerExceptionId = null;
            if (ex.InnerException != null)
            {
                innerExceptionId = ReportException(connection, ex.InnerException);
            }

            int callstackId = -1;
            try
            {
                MySqlCommand insertCallstackCommand = connection.CreateCommand();
                insertCallstackCommand.CommandType = CommandType.Text;
                insertCallstackCommand.CommandText =
                    string.Format(
                        InsertCallstack,
                        ex.TargetSite.Module.Assembly.FullName.Split(',')[0],
                        ex.Message,
                        innerExceptionId == null ? "null" : innerExceptionId.ToString());
                MySqlDataReader dataReader = insertCallstackCommand.ExecuteReader();
                if (dataReader.Read())
                {
                    callstackId = dataReader.GetInt32(0);
                    dataReader.Close();

                    using (StringReader stringReader = new StringReader(ex.StackTrace))
                    {
                        string line = null;
                        int level = 0;
                        do
                        {
                            line = stringReader.ReadLine();
                            if (!string.IsNullOrEmpty(line))
                            {
                                string file = null;
                                int? lineNumber = null;
                                string functionName = "?";
                                if (Regex.IsMatch(line, RegexFileName))
                                {
                                    file = Regex.Match(line, RegexFileName).Value;
                                    if (file.Length > 100)
                                    {
                                        file = file.Substring(file.Length - 100, 100);
                                    }
                                    file = file.Replace(@"\", @"\\");
                                }
                                if (Regex.IsMatch(line, RegexFunctionName))
                                {
                                    functionName = Regex.Match(line, RegexFunctionName).Value;
                                }
                                if (Regex.IsMatch(line, RegexLineNumber))
                                {
                                    int checkLineNumber = -1;
                                    if (int.TryParse(Regex.Match(line, RegexLineNumber).Value, out checkLineNumber))
                                    {
                                        lineNumber = checkLineNumber;
                                    }
                                }
                                ReportCallstackLevel(connection, file, lineNumber, level, callstackId, functionName);
                                level++;
                            }
                        } while (line != null);
                    }
                }
            }
            catch (MySqlException exception)
            {
                sLog.Fatal(string.Format("Failed to open connection on {0}, Database {1}.", ReportServer, ReportDatabase), exception);
                return null;
            }
            return callstackId;
        }

        private static void ReportCallstackLevel(MySqlConnection connection, string file, int? lineNumber, int level, int callstackId, string functionName)
        {
            MySqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = string.Format(
                InsertCallstackLevel,
                string.IsNullOrEmpty(file) ? "null" : "'" + file + "'",
                lineNumber == null ? "null" : lineNumber.ToString(),
                level,
                callstackId,
                functionName);
            command.ExecuteNonQuery();
        }

        private static MySqlConnection GetConnection()
        {
            MySqlConnectionStringBuilder mySqlConnectionStringBuilder = new MySqlConnectionStringBuilder();
            mySqlConnectionStringBuilder.Server = ReportServer;
            mySqlConnectionStringBuilder.UserID = ReportUsername;
            mySqlConnectionStringBuilder.Password = ReportPassword;
            mySqlConnectionStringBuilder.Database = ReportDatabase;
            MySqlConnection mySqlConnection = new MySqlConnection(mySqlConnectionStringBuilder.ConnectionString);
            try
            {
                mySqlConnection.Open();
            }
            catch (MySqlException ex)
            {
                sLog.Fatal(string.Format("Failed to open connection on {0}, Database {1}.", ReportServer, ReportDatabase), ex);
                try
                {
                    mySqlConnection.Close();
                    mySqlConnection.Dispose();
                }
                catch (Exception) { }
                return null;
            }
            return mySqlConnection;
        }

        private const string RegexFunctionName = "(?<=at ).+?((?= in)|(?=$)|(?=:))";
        private const string RegexFileName = "(?<=in ).+?((?=:line)|(?=$))";
        private const string RegexLineNumber = @"(?<=line )\d+";
        private const string ReportServer = "characterbridge.db.6369096.hostedresource.com";
        private const string ReportUsername = "characterbridge";
        //This password is only for this event logger. Generally nothing important in here, so I'll keep it in the clear.
        private const string ReportPassword = "cHarAct3rBR1dg";
        private const string ReportDatabase = "characterbridge";
        private const string InsertEvent = "CALL _INSERT_Event('{0}', '{1}', {2}, '{3}');"; //LoggerName, Message, CallstackId, SeverityName
        private const string InsertCallstack = "CALL _INSERT_Callstack('{0}', '{1}', {2});"; //AssemblyName, Message, InnerCallstackId
        private const string InsertCallstackLevel = "CALL _INSERT_CallstackLevel({0}, {1}, {2}, {3}, '{4}');"; //File, LineNumber, Level, CallstackId, FunctionName

        private static ILog sLog = LogManager.GetLogger("Bridge.Base.Utilities.BridgeReporter");
    }
}
